/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/**
 * @version comoro.c v 1.0.0
 * @author Dr.NP <np@bsgroup.org>
 * @package comoro_client
 *
 * Comoro client library
 */

#include "comoro.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <event.h>

#ifdef HAVE_MALLOC_H
#ifndef __OpenBSD__
#include <malloc.h>
#endif
#endif

struct base_list_item *base_list;
static int base_list_size;
static int total_base;

/* Client initialize */
int comoro_init(struct comoro_ptr *c)
{
	if (!c)
	{
		return RTN_MEMORY_ERROR;
	}

	memset(c, 0, sizeof(struct comoro_ptr));
	base_list = malloc(sizeof(struct base_list_item) * 256);
	base_list_size = 256;
	total_base = 0;
	
	return RTN_SUCCESS;
}

/* Make connection to server */
int comoro_connect(struct comoro_ptr *c, unsigned long addr, int port)
{
	struct sockaddr_in connect_addr;
	if (STATUS_NOT_CONNECTED != c->status && STATUS_CLOSED_LOGIN != c->status)
	{
		// Connected already, so close it first
		comoro_disconnect(c);
	}

	c->conn_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (!c->conn_fd)
	{
		return RTN_IO_ERROR;
	}

	connect_addr.sin_family = AF_INET;
	connect_addr.sin_addr.s_addr = addr;
	connect_addr.sin_port = htons(port);

	if (connect(c->conn_fd, (struct sockaddr *) &connect_addr, sizeof(connect_addr)) < 0)
	{
		return RTN_IO_ERROR;
	}

	if (!c->ev_base)
	{
		c->ev_base = event_init();
	}

	event_set(&c->ev_read, c->conn_fd, EV_READ|EV_PERSIST, comoro_on_read, (void *) c);
	event_base_set(c->ev_base, &c->ev_read);
	event_add(&c->ev_read, NULL);
	
	comoro_timer_cb(0, 0, c);
	
	c->status ++;
	if (c->on_status)
	{
		c->on_status(c->status);
	}
	
	return RTN_SUCCESS;
}

/* Disconnect from server */
int comoro_disconnect(struct comoro_ptr *c)
{
	if (STATUS_NOT_CONNECTED != c->status && STATUS_CLOSED_LOGIN != c->status)
	{
		close(c->conn_fd);
		shutdown(c->conn_fd, 2);
	}

	evtimer_del(&c->ev_timer);
	event_del(&c->ev_read);
	
	c->status = (c->status == STATUS_SELECTED_SERVER) ? STATUS_CLOSED_LOGIN : STATUS_NOT_CONNECTED;
	if (c->on_status)
	{
		c->on_status(c->status);
	}

	return RTN_SUCCESS;
}

/* Set options of comoro object */
int comoro_setopt(struct comoro_ptr *c, int opt, int value)
{
	return RTN_SUCCESS;
}

/* Add base server item to list */
int comoro_base_add(unsigned int base_id, unsigned int srv_addr, unsigned int srv_port, unsigned int num_clients)
{
	while (total_base >= base_list_size)
	{
		// Enlarge
		struct base_list_item *new_list = realloc(base_list, sizeof(struct base_list_item) * base_list_size * 2);

		if (new_list)
		{
			base_list = new_list;
			base_list_size *= 2;
		}

		else
		{
			return -1;
		}
	}
	
	base_list[total_base].base_id = base_id;
	base_list[total_base].srv_addr = srv_addr;
	base_list[total_base].srv_port = srv_port;
	base_list[total_base].num_clients = num_clients;

	total_base ++;
	
	return total_base - 1;
}

/* Return base server list */
struct base_list_item * comoro_base_list(int *total)
{
	*total = total_base;
	
	return base_list;
}

/* On data from server */
void comoro_on_read(int fd, short ev, void *arg)
{
	struct comoro_ptr *c = (struct comoro_ptr *) arg;
	int res;
	unsigned int buffer_avail = 0;
	unsigned int buffer_curr = 0;
	char *new_buffer;

	if (!c || !c->conn_fd || fd != c->conn_fd)
	{
		return;
	}

	if (!c->read_buffer)
	{
		// Initialize buffer
		c->read_buffer = (char *) malloc(CONNECTOR_READ_BUFFER_SIZE);
		c->buffer_size = buffer_avail = CONNECTOR_READ_BUFFER_SIZE;
		memset(c->read_buffer, 0, CONNECTOR_READ_BUFFER_SIZE);
	}

	else
	{
		buffer_avail = c->buffer_size;
		c->buffer_data_size = 0;
	}

	while(1)
	{
		res = read(c->conn_fd, c->read_buffer + buffer_curr, buffer_avail);

		if (res > 0)
		{
			if ((unsigned int) res == buffer_avail)
			{
				// Buffer full, maybe stream available, enlarge buffer
				if ((new_buffer = (char *) realloc(c->read_buffer, c->buffer_size * 2)) == NULL)
				{
					comoro_disconnect(c);
					
					return;
				}
				
				buffer_avail = c->buffer_size;
				c->read_buffer = new_buffer;
				memset(c->read_buffer + c->buffer_size, 0, c->buffer_size);
				c->buffer_size *= 2;
			}
			
			else
			{
				// Data read completely
				buffer_avail -= res;
			}

			c->buffer_data_size += res;
			
			break;
		}

		if (-1 == res)
		{
			// Read error
			comoro_disconnect(c);
			
			return;
		}
		
		if (0 == res && 0 == c->buffer_data_size)
		{
			comoro_disconnect(c);
			
			return;
		}
	}

	// Loop data
	unsigned int data_remain = c->buffer_data_size;
	unsigned int plen = 0;
	unsigned int spa = 0;
	unsigned int seq_id = 0;
	unsigned int base_id = 0;
	unsigned int srv_addr = 0;
	unsigned int srv_port = 0;
	unsigned int num_clients = 0;
	static char signal[8];
	char *edata;
	char **p;
	int cmd_id;
	unsigned s_plen = 0;

	//package_output(c->read_buffer, data_remain);
	while (data_remain > 0)
	{
		if (data_remain >= 4)
		{
			plen = get_u_int(c->read_buffer + c->buffer_data_size - data_remain);
			if (plen > 0xFFFFFF00)
			{
				if (0xFFFFFFFF == plen && data_remain >= 8)
				{
					// Heartbeat response
					data_remain -= 8;
				}

				else if (0xFFFFFFFE == plen && data_remain >= 8)
				{
					// Sequence response
					spa = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
					data_remain -= 8;
				}

				else if (0xFFFFFFFD == plen && data_remain >= 8)
				{
					// Sequence request
					spa = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
					data_remain -= 8;
				}

				else if (0xFFFFFFFB == plen && data_remain >= 8 + SESSION_KEY_LENGTH)
				{
					// Login result
					if (c->status == STATUS_PRE_LOGIN)
					{
						spa = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
						memcpy(c->session, c->read_buffer + c->buffer_data_size - data_remain + 8, SESSION_KEY_LENGTH);

						c->status = (spa) ? STATUS_LOGIN_SUCCESS : STATUS_LOGIN_FAILED;
						if (c->on_status)
						{
							c->on_status(c->status);
						}
					}
					
					data_remain -= 8 + SESSION_KEY_LENGTH;
				}

				else if (0xFFFFFFFA == plen && data_remain >= 20)
				{
					// Base server list item
					if (c->status == STATUS_LOGIN_SUCCESS)
					{
						base_id = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
						srv_addr = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 8);
						srv_port = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 12);
						num_clients = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 16);

						if (base_id == 0xFFFFFFFF)
						{
							// List ended
							c->status = STATUS_LISTED_SERVER;
							if (c->on_status)
							{
								c->on_status(c->status);
							}
						}

						else
						{
							// Add into list
							comoro_base_add(base_id, srv_addr, srv_port, num_clients);
						}
					}
					
					data_remain -= 20;
				}

				else if (0xFFFFFFF5 == plen && data_remain >= 8)
				{
					// Select server signal
					if (c->status == STATUS_LISTED_SERVER)
					{
						c->status = STATUS_SELECTED_SERVER;
						if (c->on_status)
						{
							c->on_status(c->status);
						}
					}
					
					data_remain -= 8;
				}

				else if (0xFFFFFFF4 == plen && data_remain >= 8)
				{
					// Verify result
					spa = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
					c->status = (spa) ? STATUS_SESSION_SUCCESS : STATUS_SESSION_FAILED;
					if (c->on_status)
					{
						c->on_status(c->status);
					}
					
					data_remain -= 8;
				}

				else
				{
					data_remain = 0;
					break;
				}
			}

			else
			{
				// Standard command
				if (4 >= plen)
				{
					break;
				}
				
				seq_id = get_u_int(c->read_buffer + c->buffer_data_size - data_remain + 4);
				
				if (seq_id > c->last_rcv_id + 1)
				{
					// Package missed
					put_u_int(0xFFFFFFFD, signal);
					put_u_int(c->last_rcv_id + 1, signal + 4);
					send(c->conn_fd, signal, 8, 0);
				}

				else
				{
					if (c->on_command)
					{
						edata = package_decode(c->read_buffer + c->buffer_data_size - data_remain + 8, plen - 8, 0);
						cmd_id = (int) get_u_int(edata);
						p = package_unpack(edata + 4, plen - 12, &s_plen);
						c->on_command(cmd_id, s_plen, p);
						
						free(edata);

						if (p)
						{
							for (spa = 0; spa < s_plen; spa ++)
							{
								if (p[spa])
								{
									free(p[spa]);
								}
							}
	
							free(p);
						}
					}

					put_u_int(0xFFFFFFFE, signal);
					put_u_int(seq_id, signal + 4);
					send(c->conn_fd, signal, 8, 0);
				}

				data_remain -= plen;
			}
		}
	}
	
	memset(c->read_buffer, 0, c->buffer_size);

	return;
}

/* Timer callback */
void comoro_timer_cb(int fd, short ev, void *arg)
{
	struct timeval t = {.tv_sec = 1, .tv_usec = 0};
	static char initialized = 0x0;
	struct comoro_ptr *c = (struct comoro_ptr *) arg;

	if (initialized)
	{
		evtimer_del(&c->ev_timer);
	}
	
	else
	{
		initialized = 0x1;
	}

	evtimer_set(&c->ev_timer, comoro_timer_cb, (void *) c);
	event_base_set(c->ev_base, &c->ev_timer);
	evtimer_add(&c->ev_timer, &t);

	comoro_hb(c);
	
	return;
}

/* Send heartbeat */
int comoro_hb(struct comoro_ptr *c)
{
	static char signal[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0};
	
	if (STATUS_NOT_CONNECTED != c->status && STATUS_CLOSED_LOGIN != c->status)
	{
		send(c->conn_fd, signal, 8, 0);
	}

	return RTN_SUCCESS;
}

/* Login process */
int comoro_login(struct comoro_ptr *c, char *username, char *password, int runner_id)
{
	static char signal[MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH + 8];

	if (STATUS_PRE_LOGIN != c->status)
	{
		return RTN_GENERAL;
	}
	
	memset(signal, 0, MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH + 8);

	strncpy(signal + 4, username, MAX_USERNAME_LENGTH);
	strncpy(signal + 4 + MAX_USERNAME_LENGTH, password, MAX_PASSWORD_LENGTH);
	put_u_int(runner_id, signal + 4 + MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH);

	put_u_int(0xFFFFFFFB, signal);

	send(c->conn_fd, signal, MAX_USERNAME_LENGTH + MAX_PASSWORD_LENGTH + 8, 0);
	
	return RTN_SUCCESS;
}

/* Get base server list */
int comoro_get_base_list(struct comoro_ptr *c)
{
	static char signal[8];

	if (STATUS_LOGIN_SUCCESS != c->status)
	{
		return RTN_GENERAL;
	}

	put_u_int(0xFFFFFFFA, signal);
	put_u_int(0, signal + 4);

	send(c->conn_fd, signal, 8, 0);
	
	return RTN_SUCCESS;
}

/* Select base server */
int comoro_select_server(struct comoro_ptr *c, int base_id)
{
	static char signal[9];

	if (STATUS_LISTED_SERVER != c->status)
	{
		return RTN_GENERAL;
	}

	put_u_int(0xFFFFFFF5, signal);
	put_u_int(base_id, signal + 4);

	send(c->conn_fd, signal, 8, 0);
	
	return RTN_SUCCESS;
}

/* Send session to base server */
int comoro_validate_session(struct comoro_ptr *c, int runner_id)
{
	static char signal[8 + SESSION_KEY_LENGTH];

	if (STATUS_PRE_SESSION != c->status)
	{
		return RTN_GENERAL;
	}

	put_u_int(0xFFFFFFF4, signal);
	put_u_int(runner_id, signal + 4);
	memcpy(signal + 8, c->session, SESSION_KEY_LENGTH);

	send(c->conn_fd, signal, 8 + SESSION_KEY_LENGTH, 0);
	
	return RTN_SUCCESS;
}

/* Send command */
int comoro_psend(struct comoro_ptr *c, int cmd_id, int plen, char **params)
{
	if (STATUS_SESSION_SUCCESS != c->status)
	{
		return RTN_GENERAL;
	}

	int s_len;
	char *stream = package_pack(params, plen, &s_len);
	put_u_int((unsigned int) cmd_id, stream);
	char *edata = package_encode(stream, s_len, 8);
	put_u_int(s_len + 8, edata);
	put_u_int(++ c->last_seq_id, edata + 4);
	send(c->conn_fd, edata, s_len + 8, 0);
	
	free(stream);
	free(edata);
	
	return RTN_SUCCESS;
}
